/*
 * Copyright 2010 Impetus Infotech.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.impetus.kundera.ejb;

import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.persistence.Entity;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.spi.PersistenceUnitInfo;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.impetus.kundera.cache.Cache;
import com.impetus.kundera.cache.CacheException;
import com.impetus.kundera.cache.CacheProvider;
import com.impetus.kundera.classreading.ClasspathReader;
import com.impetus.kundera.classreading.Reader;
import com.impetus.kundera.metadata.MetadataManager;
import com.impetus.kundera.proxy.EnhancedEntity;
import com.impetus.kundera.proxy.EntityEnhancerFactory;
import com.impetus.kundera.proxy.KunderaProxy;
import com.impetus.kundera.proxy.LazyInitializerFactory;
import com.impetus.kundera.proxy.cglib.CglibEntityEnhancerFactory;
import com.impetus.kundera.proxy.cglib.CglibLazyInitializerFactory;

/**
 * The Class EntityManagerFactoryImpl.
 * 
 * @author animesh.kumar
 */
public class EntityManagerFactoryImpl implements EntityManagerFactory {

    /** the log used by this class. */
    private static Log log = LogFactory.getLog(EntityManagerFactoryImpl.class);

    /** The Constant propsFileName. */
    private static final String propsFileName = "/kundera.properties";

    /** Whether or not the factory has been closed. */
    private boolean closed = false;

    /** Also the prefix that will be applied to each Domain. */
    private String persistenceUnitName;

    /** properties file values. */
    @SuppressWarnings("unchecked")
    private Map props;

    /** The sessionless. */
    private boolean sessionless;

    /** The metadata manager. */
    private MetadataManager metadataManager;

    /** The nodes. */
    private String[] nodes;

    /** The port. */
    private Integer port;

    /** The keyspace. */
    private String keyspace;
    
    private List<String> classes;

    /** The Cache provider. */
    private CacheProvider cacheProvider;
    
    private String cacheProviderClassName;
    
    /** The enhanced proxy factory. */
    private EntityEnhancerFactory enhancedProxyFactory;
    
    /** The lazy initializer factory. */
    private LazyInitializerFactory lazyInitializerFactory;
    
    /**
     * A convenience constructor.
     * 
     * @param persistenceUnitName
     *            used to prefix the Cassandra domains
     */
    public EntityManagerFactoryImpl(String persistenceUnitName) {
        this(persistenceUnitName, null);
    }
    
  
//    /**
//     * Parameterize constructor to load configuration for no sql databases other than Cassandra.  
//     * @param persistenceUnitName
//     * @param isCassandra
//     */
//    public EntityManagerFactoryImpl(String persistenceUnitName, boolean isCassandra){
//          onLoad(persistenceUnitName);
//        if(isCassandra) {
//            loadCassandraProps();
//            initCassandraClient();
//        } else {
//        	try{
//        		loadProperties(propsFileName);
//        	}catch(IOException ioex) {
//        		throw new PersistenceException(ioex.getMessage());
//        	}
//        	loadHBase();
//        }
//        
//    	// Second level cache
//		initSecondLevelCache();
//    }

    /**
     * This one is generally called via the PersistenceProvider.
     * 
     * @param persistenceUnitInfo
     *            only using persistenceUnitName for now
     * @param props
     *            the props
     */
    public EntityManagerFactoryImpl(PersistenceUnitInfo persistenceUnitInfo, Map props) {
        this(persistenceUnitInfo != null ? persistenceUnitInfo.getPersistenceUnitName() : null, props);
    }

    /**
     * Use this if you want to construct this directly.
     * 
     * @param persistenceUnitName
     *            used to prefix the Cassandra domains
     * @param props
     *            should have accessKey and secretKey
     */
    public EntityManagerFactoryImpl(String persistenceUnitName, Map props) {
       onLoad(persistenceUnitName);
      this.props = props;
//        // if props is NULL or empty, look for kundera.properties and populate
//        if (props == null || props.isEmpty()) {
//            loadCassandraProps();
//            initCassandraClient();
//        }
//        //configure Cassandra client.
    	// Second level cache
//		initSecondLevelCache();
    }
    
    public EntityManagerFactoryImpl(PersistenceMetadata metaData, Map props) {
    	this.classes = metaData.getClasses();
    	onLoad(metaData.getName());
    	this.props = props;    	
    }


//    /**
//     * Loads cassandra properties.
//     */
//	private void loadCassandraProps() {
//		try {
//			log.debug("Trying to load Kundera Properties from " + propsFileName);
//		    loadProperties(propsFileName);
//		} catch (IOException e) {
//		    throw new PersistenceException(e);
//		}
//	}
	
//	/**
//	 * Loads HBase properties.
//	 */
//	private void loadHBase() {
//		client = new HBaseClient();
//		client.setContactNodes("localhost");
//		client.setDefaultPort(6000);
//		client.connect();
//	}

    /**
     * Method to instantiate persistence entities and metadata.
     * @param persistenceUnitName
     */
	private void onLoad(String persistenceUnitName) {
		if (persistenceUnitName == null) {
            throw new IllegalArgumentException("Must have a persistenceUnitName!");
        }
        long start = System.currentTimeMillis();

        this.persistenceUnitName = persistenceUnitName;
        metadataManager = new MetadataManager(this);

        // scan classes for @Entity
        Reader reader = new ClasspathReader(this.classes);
        reader.addValidAnnotations(Entity.class.getName());
        reader.addAnnotationDiscoveryListeners(metadataManager);
        reader.read();

        metadataManager.build();
        
        enhancedProxyFactory = new CglibEntityEnhancerFactory();
        lazyInitializerFactory = new CglibLazyInitializerFactory();
        
        log.info("EntityManagerFactoryImpl loaded in " + (System.currentTimeMillis() - start) + "ms.");
	}

    
//    /**
//     * Load properties.
//     * 
//     * @param propsFileName
//     *            the props file name
//     * 
//     * @throws IOException
//     *             Signals that an I/O exception has occurred.
//     */
//    private void loadProperties(String propsFileName) throws IOException {
//        Properties props_ = new Properties();
//        InputStream stream = this.getClass().getResourceAsStream(propsFileName);
//        if (stream == null) {
//            throw new FileNotFoundException(propsFileName + " not found on classpath. Could not initialize Kundera.");
//        }
//        props_.load(stream);
//        props = props_;
//        stream.close();
//    }

//    /**
//     * Inits the.
//     */
//    private void initCassandraClient() {
//    	// Look for kundera.nodes
//    	try {
//    		String kunderaNodes = (String)props.get("kundera.nodes");
//    		if (null == kunderaNodes || kunderaNodes.isEmpty()) {
//    			throw new IllegalArgumentException();
//    		}
//    		nodes = kunderaNodes.split(",");
//    	} catch (Exception e) {
//    		throw new IllegalArgumentException("Mandatory property missing 'kundera.nodes'");
//    	}
//        
//    	// kundera.port
//    	String kunderaPort = (String) props.get("kundera.port");
//		if (null == kunderaPort || kunderaPort.isEmpty()) {
//			throw new IllegalArgumentException("Mandatory property missing 'kundera.port'");
//		}
//    	try {
//    		port = Integer.parseInt(kunderaPort);
//    	} catch (Exception e) {
//    		throw new IllegalArgumentException("Invalid value for property 'kundera.port': " + kunderaPort + ". (Should it be 9160?)");
//    	}
//        
//    	// kundera.keyspace
//    	keyspace = (String) props.get("kundera.keyspace");
//		if (null == keyspace || keyspace.isEmpty()) {
//			throw new IllegalArgumentException("Mandatory property missing 'kundera.keyspace'");
//		}
//        
//        // kundera.client
//        String cassandraClient = (String) props.get("kundera.client");
//		if (null == cassandraClient || cassandraClient.isEmpty()) {
//			throw new IllegalArgumentException("Mandatory property missing 'kundera.client'");
//		}
//	
//		
//		// Instantiate the client
//        try {
//    		if ( cassandraClient.endsWith( ".class" ) ) {
//    			cassandraClient = cassandraClient.substring( 0, cassandraClient.length() - 6 );
//    		}
//    		
//            client = (CassandraClient) Class.forName(cassandraClient).newInstance();
//            client.setContactNodes(nodes);
//            client.setDefaultPort(port);
//        } catch (Exception e) {
//            throw new IllegalArgumentException("Invalid value for property 'kundera.client': " + cassandraClient + ". (Should it be com.impetus.kundera.client.PelopsClient?");
//        }
//        
//        log.info("Connecting to Cassandra... (nodes:" + Arrays.asList(nodes) + ", port:" + port + ", keyspace:" + keyspace + ")");
//        
//        // connect to Cassandra DB
//        client.connect();
//    }

//    /**
//	 * Inits the second level cache.
//	 */
//    @SuppressWarnings("unchecked")
//	private void initSecondLevelCache() {
////    	String cacheProviderClassName = (String) props.get("kundera.cache.provider_class");
//        if (cacheProviderClassName != null) {
//            try {
//                Class<CacheProvider> cacheProviderClass = (Class<CacheProvider>) Class.forName(cacheProviderClassName);
//                cacheProvider = cacheProviderClass.newInstance();
//                cacheProvider.init(props);
//            } catch (Exception e) {
//                throw new RuntimeException(e);
//            }
//        }
//        if (cacheProvider == null) {
//        	cacheProvider = new NonOperationalCacheProvider();
//        }
//        log.info("Initialized second-level cache. Provider: " + cacheProvider.getClass());
//    }
    
    /**
	 * Gets the cache.
	 * 
	 * @param entity
	 *            the entity
	 * @return the cache
	 */
    public Cache getCache(Class<?> entity) {
        try {
            String cacheName = metadataManager.getEntityMetadata(entity).getEntityClazz().getName();
            return cacheProvider.createCache(cacheName);
        } catch (CacheException e) {
            throw new RuntimeException(e);
        }
    }

    
    /**
     * Gets the metadata manager.
     * 
     * @return the metadataManager
     */
    public final MetadataManager getMetadataManager() {
        return metadataManager;
    }

    /* @see javax.persistence.EntityManagerFactory#close() */
    @Override
    public final void close() {
        closed = true;
//        client.shutdown();
        cacheProvider.shutdown();
    }

    /* @see javax.persistence.EntityManagerFactory#createEntityManager() */
    @Override
    public final EntityManager createEntityManager() {
        return new EntityManagerImpl(this);
    }

    /* @see javax.persistence.EntityManagerFactory#createEntityManager(java.util.Map) */
    @Override
    public final EntityManager createEntityManager(Map map) {
        return new EntityManagerImpl(this);
    }

    /**
	 * Gets the enhanced entity.
	 * 
	 * @param entity
	 *            the entity
	 * @param id
	 *            the id
	 * @param foreignKeyMap
	 *            the foreign key map
	 * @return the enhanced entity
	 */
    public EnhancedEntity getEnhancedEntity (Object entity, String id, 
			Map<String, Set<String>> foreignKeyMap) {
    	return enhancedProxyFactory.getProxy(entity, id, foreignKeyMap);
    }

    /**
	 * Gets the lazy entity.
	 * 
	 * @param entityName
	 *            the entity name
	 * @param persistentClass
	 *            the persistent class
	 * @param getIdentifierMethod
	 *            the get identifier method
	 * @param setIdentifierMethod
	 *            the set identifier method
	 * @param id
	 *            the id
	 * @param em
	 *            the em
	 * @return the lazy entity
	 */
    public KunderaProxy getLazyEntity (String entityName,
			Class<?> persistentClass,
			Method getIdentifierMethod, Method setIdentifierMethod, String id,
			EntityManagerImpl em) {
    	return lazyInitializerFactory.getProxy(
    			entityName, 
    			persistentClass,  
    			getIdentifierMethod, 
    			setIdentifierMethod, 
				id, 
				em);
    }
    
    /* @see javax.persistence.EntityManagerFactory#isOpen() */
    @Override
    public final boolean isOpen() {
        return !closed;
    }

    /**
     * Gets the persistence unit name.
     * 
     * @return the persistence unit name
     */
    public final String getPersistenceUnitName() {
        return persistenceUnitName;
    }

    /**
     * Gets the nodes.
     * 
     * @return the nodes
     */
    public String[] getNodes() {
        return nodes;
    }

    /**
     * Gets the port.
     * 
     * @return the port
     */
    public int getPort() {
        return port;
    }

    /**
     * Gets the keyspace.
     * 
     * @return the keyspace
     */
    public String getKeyspace() {
        return keyspace;
    }

	/**
	 * @return the classes
	 */
	public List<String> getClasses() {
		return classes;
	}

	/**
	 * @param classes the classes to set
	 */
	public void setClasses(List<String> classes) {
		this.classes = classes;
	}  
    
}
